
package com.ideacome.test.common.mock.config.annotation;

import com.ideacome.test.common.mock.interceptor.MockableOperation;
import org.springframework.cache.annotation.CacheAnnotationParser;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.SpringCacheAnnotationParser;
import org.springframework.cache.interceptor.CacheOperation;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.StringUtils;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;

/**
 * 读取 {@link Mockable}注解 加入到缓存操作里面
 *
 * @author 罗成
 */
public class SpringMockAnnotationParser implements CacheAnnotationParser, Serializable {

    @Override
    public Collection<CacheOperation> parseCacheAnnotations(Class<?> type) {
        DefaultCacheConfig defaultConfig = getDefaultCacheConfig(type);
        return parseCacheAnnotations(defaultConfig, type);
    }

    @Override
    public Collection<CacheOperation> parseCacheAnnotations(Method method) {
        DefaultCacheConfig defaultConfig = getDefaultCacheConfig(method.getDeclaringClass());
        return parseCacheAnnotations(defaultConfig, method);
    }

    protected Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) {
        Collection<CacheOperation> ops = null;
        Collection<Mockable> mockables = AnnotatedElementUtils.getAllMergedAnnotations(ae, Mockable.class);
        if (!mockables.isEmpty()) {
            ops = lazyInit(ops);
            for (Mockable mockable : mockables) {
                ops.add(parseMockableAnnotation(ae, cachingConfig, mockable));
            }
        }
        return ops;
    }

    private <T extends Annotation> Collection<CacheOperation> lazyInit(Collection<CacheOperation> ops) {
        return (ops != null ? ops : new ArrayList<CacheOperation>(1));
    }

    MockableOperation parseMockableAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, Mockable mockable) {
        MockableOperation.Builder builder = new MockableOperation.Builder();
        builder.setName(ae.toString());
        builder.setKey(mockable.key());
        builder.setCacheNames(mockable.cacheNames());
        builder.setKeyGenerator(mockable.keyGenerator());
        defaultConfig.applyDefault(builder);
        MockableOperation op = builder.build();
        validateCacheOperation(ae, op);
        return op;
    }

    /**
     * Provides the {@link DefaultCacheConfig} instance for the specified {@link Class}.
     *
     * @param target the class-level to handle
     * @return the default config (never {@code null})
     */
    DefaultCacheConfig getDefaultCacheConfig(Class<?> target) {
        CacheConfig annotation = AnnotatedElementUtils.getMergedAnnotation(target, CacheConfig.class);
        if (annotation != null) {
            return new DefaultCacheConfig(annotation.cacheNames(), annotation.keyGenerator(),
                    annotation.cacheManager(), annotation.cacheResolver());
        }
        return new DefaultCacheConfig();
    }

    /**
     * Validates the specified {@link CacheOperation}.
     * <p>Throws an {@link IllegalStateException} if the state of the operation is
     * invalid. As there might be multiple sources for default values, this ensure
     * that the operation is in a proper state before being returned.
     *
     * @param ae        the annotated element of the cache operation
     * @param operation the {@link CacheOperation} to validate
     */
    private void validateCacheOperation(AnnotatedElement ae, CacheOperation operation) {
        if (StringUtils.hasText(operation.getKey()) && StringUtils.hasText(operation.getKeyGenerator())) {
            throw new IllegalStateException("Invalid cache annotation configuration on '" +
                    ae.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
                    "These attributes are mutually exclusive: either set the SpEL expression used to" +
                    "compute the key at runtime or set the name of the KeyGenerator bean to use.");
        }
        if (StringUtils.hasText(operation.getCacheManager()) && StringUtils.hasText(operation.getCacheResolver())) {
            throw new IllegalStateException("Invalid cache annotation configuration on '" +
                    ae.toString() + "'. Both 'cacheManager' and 'cacheResolver' attributes have been set. " +
                    "These attributes are mutually exclusive: the cache manager is used to configure a" +
                    "default cache resolver if none is set. If a cache resolver is set, the cache manager" +
                    "won't be used.");
        }
    }

    @Override
    public boolean equals(Object other) {
        return (this == other || other instanceof SpringCacheAnnotationParser);
    }

    @Override
    public int hashCode() {
        return SpringCacheAnnotationParser.class.hashCode();
    }


    /**
     * Provides default settings for a given set of cache operations.
     */
    static class DefaultCacheConfig {

        private final String[] cacheNames;

        private final String keyGenerator;

        private final String cacheManager;

        private final String cacheResolver;

        public DefaultCacheConfig() {
            this(null, null, null, null);
        }

        private DefaultCacheConfig(String[] cacheNames, String keyGenerator, String cacheManager, String cacheResolver) {
            this.cacheNames = cacheNames;
            this.keyGenerator = keyGenerator;
            this.cacheManager = cacheManager;
            this.cacheResolver = cacheResolver;
        }

        /**
         * Apply the defaults to the specified {@link CacheOperation.Builder}.
         *
         * @param builder the operation builder to update
         */
        public void applyDefault(CacheOperation.Builder builder) {
            if (builder.getCacheNames().isEmpty() && this.cacheNames != null) {
                builder.setCacheNames(this.cacheNames);
            }
            if (!StringUtils.hasText(builder.getKey()) && !StringUtils.hasText(builder.getKeyGenerator()) &&
                    StringUtils.hasText(this.keyGenerator)) {
                builder.setKeyGenerator(this.keyGenerator);
            }

            if (StringUtils.hasText(builder.getCacheManager()) || StringUtils.hasText(builder.getCacheResolver())) {
                // One of these is set so we should not inherit anything
            } else if (StringUtils.hasText(this.cacheResolver)) {
                builder.setCacheResolver(this.cacheResolver);
            } else if (StringUtils.hasText(this.cacheManager)) {
                builder.setCacheManager(this.cacheManager);
            }
        }

    }

}
